/*
 * Copyright 2015-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
@file:Suppress("EXTENSION_SHADOWED_BY_MEMBER")

package com.willowtreeapps.opentest4k

import kotlin.reflect.KClass

/**
 * Serializable representation of a value that was used in an assertion.
 *
 * This class only stores the value if it implements [Serializable].
 * In any case, it stores its [runtime type][type], [ ][identityHashCode], and [ ][stringRepresentation] determined via [ ][String.valueOf]. If the invocation of `String.valueOf(Object)`
 * throws an [Exception], the string representation will take the form of
 * `"<Exception in toString(): " + e + ">"`, where "e" is the caught
 * exception.
 *
 *
 * The [toString] method returns the string representation of the
 * value along with its type and identity hash code.
 *
 * @author Marc Philipp
 * @author Sam Brannen
 * @since 1.0
 */
actual class ValueWrapper private constructor(
    /**
     * Returns the value supplied to [.create] if the value
     * implements [Serializable]; otherwise, `null`.
     *
     * @see .getEphemeralValue
     */
    val value: Any?,

    /**
     * Returns the value's string representation.
     *
     *
     * The string representation is generated by invoking
     * [String.valueOf(value)][String.valueOf] for the value
     * supplied to [.create].
     *
     * @see .getValue
     */
    stringRepresentation: String? = null
) {

    val stringRepresentation: String = stringRepresentation ?: safeValueToString(value)

    /**
     * Returns the value's runtime type or `null` if the value is
     * `null`.
     */
    val valueType: KClass<*>? = if (value != null) value::class else null

    /**
     * Returns the value's identity hash code.
     *
     *
     * The identity hash code is generated by invoking
     * [System.identityHashCode(value)][System.identityHashCode]
     * for the value supplied to [.create].
     *
     * @see .getValue
     */
    val identityHashCode: Int = value.hashCode()

    /**
     * Returns the original value supplied to [create()][.create].
     *
     *
     * If this `ValueWrapper` was created by deserialization this method
     * returns `null`.
     *
     * @see .getValue
     * @since 1.2
     */
    val ephemeralValue: Any? = value

    /**
     * Returns the value's string representation along with its type and
     * identity hash code.
     */
    override fun toString(): String {
        if (valueType == null) {
            return "null"
        }
        return "$stringRepresentation (${valueType.simpleName}@$identityHashCode)"
    }

    companion object {
        private val nullValueWrapper: ValueWrapper = ValueWrapper(null)

        /**
         * Factory for creating a new `ValueWrapper` for the supplied `value`.
         *
         *
         * If the supplied `value` is `null`, this method will return a
         * cached `ValueWrapper` suitable for all `null` values.
         * If the supplied `value` is already an instance of [ValueWrapper],
         * it will be returned as is.
         *
         * @param value the value to wrap; may be `null`
         * @return a wrapper for the supplied value; never `null`
         */
        fun create(value: Any?): ValueWrapper {
            if (value is ValueWrapper) return value
            return if ((value == null)) nullValueWrapper else ValueWrapper(
                value
            )
        }

        /**
         * Factory for creating a new `ValueWrapper` for the supplied `value`
         * using the supplied custom `stringRepresentation`.
         *
         *
         * You should use this method when you don't want to rely on the result of the
         * value's [toString()][Any.toString] method.
         *
         *
         * If the supplied `value` is `null`, this method will return a
         * cached `ValueWrapper` suitable for all `null` values.
         * If the supplied `value` is already an instance of [ValueWrapper],
         * it will be returned as is if the `stringRepresentation` match, otherwise
         * the original value will be unwrapped and a new `ValueWrapper` with the
         * new `stringRepresentation` will be created.
         *
         * @param value the value to wrap; may be `null`
         * @param stringRepresentation a custom rendering of the value; will fallback to
         * the default behavior if `null`
         * @return a wrapper for the supplied value; never `null`
         * @since 1.2
         */
        fun create(value: Any?, stringRepresentation: String?): ValueWrapper {
            if (value is ValueWrapper) {
                val wrapper: ValueWrapper = value
                return if ((wrapper.stringRepresentation == stringRepresentation)) wrapper else create(
                    wrapper.value,
                    stringRepresentation
                )
            }
            return (if (value == null) nullValueWrapper else ValueWrapper(
                value,
                stringRepresentation
            ))
        }

        private fun safeValueToString(value: Any?): String {
            return try {
                value.toString()
            } catch (e: Exception) {
                "<Exception in toString(): $e>"
            }
        }
    }
}

actual fun Any?.toValueWrapper(): ValueWrapper = ValueWrapper.create(this)

actual fun Any?.toValueWrapper(stringRepresentation: String?): ValueWrapper =
    ValueWrapper.create(this, stringRepresentation)

actual inline val ValueWrapper.value: Any? get() = throw NotImplementedError()
actual inline val ValueWrapper.valueType: KClass<*>? get() = throw NotImplementedError()
actual inline val ValueWrapper.ephemeralValue: Any? get() = throw NotImplementedError()
actual inline val ValueWrapper.stringRepresentation: String get() = throw NotImplementedError()
actual inline val ValueWrapper.identityHashCode: Int get() = throw NotImplementedError()


/**
 * @author Sam Brannen
 * @author Marc Philipp
 */
actual open class AssertionFailedError : AssertionError {

    private val _cause: Throwable?
    val expected: ValueWrapper?
    val actual: ValueWrapper?

    actual constructor() : this(null)

    actual constructor(message: String?) : this(message, null)

    actual constructor(message: String?, cause: Throwable?) : this(message, null, null, cause)

    actual constructor(message: String?, expected: Any?, actual: Any?) : this(
        message,
        expected.toValueWrapper(),
        actual.toValueWrapper(),
        null
    )

    actual constructor(message: String?, expected: Any?, actual: Any?, cause: Throwable?) : this(
        message,
        expected.toValueWrapper(),
        actual.toValueWrapper(),
        cause
    )

    private constructor(message: String?, expected: ValueWrapper?, actual: ValueWrapper?, cause: Throwable?) : super(
        if (message.isNullOrBlank()) "" else message
    ) {
        this._cause = cause
        this.expected = expected
        this.actual = actual
    }

    override val cause: Throwable?
        get() = _cause

    val isExpectedDefined: Boolean get() = expected != null
    val isActualDefined: Boolean get() = actual != null

    override fun toString(): String {
        return buildString {
            append(className())
            val m = message
            if (!m.isNullOrEmpty()) {
                append(": ")
                append(m)
            }
        }
    }
}

actual inline val AssertionFailedError.isExpectedDefined: Boolean get() = throw NotImplementedError()
actual inline val AssertionFailedError.isActualDefined: Boolean get() = throw NotImplementedError()
actual inline val AssertionFailedError.expected: ValueWrapper? get() = throw NotImplementedError()
actual inline val AssertionFailedError.actual: ValueWrapper? get() = throw NotImplementedError()

/**
 * @author Johannes Link
 * @author Sam Brannen
 * @author Marc Philipp
 */
actual open class MultipleFailuresError actual constructor(heading: String?, val failures: List<Throwable>) :
    AssertionError() {

    val heading: String = if (heading.isNullOrBlank()) "Multiple Failures" else heading.trim()

    override val message: String?
        get() {
            val failureCount = failures.size

            if (failureCount == 0) {
                return heading
            }

            return buildString {
                append(heading)
                append(" (")
                append(failureCount).append(" ")
                append(pluralize(failureCount, "failure", "failures"))
                append(")\n")

                val lastIndex = failureCount - 1
                for (failure in failures.subList(0, lastIndex)) {
                    append("\t")
                    append(nullSafeMessage(failure))
                    append("\n")
                }
                append('\t')
                append(nullSafeMessage(failures[lastIndex]))
            }
        }

    actual fun hasFailures(): Boolean = failures.isNotEmpty()
}

@Suppress("NOTHING_TO_INLINE")
private inline fun pluralize(count: Int, singular: String, plural: String): String =
    if (count == 1) singular else plural

private fun nullSafeMessage(failure: Throwable): String {
    if (failure.message.isNullOrBlank()) {
        return failure.className() + ": <no message>"
    }
    return failure.className() + ": " + failure.message!!
}

actual inline val MultipleFailuresError.failures: List<Throwable> get() = throw NotImplementedError()

/**
 * @author Johannes Link
 * @author Sam Brannen
 */
actual open class IncompleteExecutionException : RuntimeException {

    actual constructor() : super()

    actual constructor(message: String?) : super(message)
    actual constructor(message: String?, cause: Throwable?): super(message, cause)
}

/**
 * @author Sam Brannen
 * @author Johannes Link
 */
actual open class TestAbortedException  : IncompleteExecutionException {

    @Suppress("unused") // Actual implementation
    actual constructor() : super()

    @Suppress("unused") // Actual implementation
    actual constructor(message: String?) : super(message)

    @Suppress("unused") // Actual implementation
    actual constructor(message: String?, cause: Throwable?): super(message, cause)
}

/**
 * @author Sam Brannen
 * @author Johannes Link
 */
actual open class TestSkippedException  : IncompleteExecutionException {

    @Suppress("unused") // Actual implementation
    actual constructor() : super()

    @Suppress("unused") // Actual implementation
    actual constructor(message: String?) : super(message)

    @Suppress("unused") // Actual implementation
    actual constructor(message: String?, cause: Throwable?): super(message, cause)
}

/**
 * @author Marc Philipp
 * @author Reinhold Degenfellner
 * @since 1.3
 */
actual open class FileInfo actual constructor(val path: String, val contents: ByteArray) {
    init {
        require(path.isNotBlank()) { "path must not be blank" }
    }

    fun contentsAsString(): String = contents.decodeToString()
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false
        other as FileInfo
        if (path != other.path) return false
        if (!contents.contentEquals(other.contents)) return false
        return true
    }

    override fun hashCode(): Int {
        return path.hashCode()
    }

    override fun toString(): String {
        return "FileInfo[path='$path', contents containing ${contents.size} bytes]"
    }
}

actual inline val FileInfo.path: String get() = throw NotImplementedError()

actual inline val FileInfo.contents: ByteArray get() = throw NotImplementedError()

actual inline fun FileInfo.contentsAsString(): String = throw NotImplementedError()